// ============================ lwip_http_api.c ============================
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "lwipopts.h"
#include "lwip/apps/httpd.h"
#include "lwip/apps/fs.h"
#include "lwip/pbuf.h"
#include "lwip_http_api.h"
#include <stdarg.h>

#define HTTP_CTX_MAX           8            // concurrent dynamic responses
#define HTTP_OUTBUF_SIZE       1024         // per-response body size
#define HTTP_HEADER_MAX        256          // max header we compose
#define HTTP_ROUTE_MAX         24


// ====== Internal state ======
typedef struct { http_method_t m; const char* path; route_fn fn; } route_t;
static route_t   g_routes[HTTP_ROUTE_MAX];
static int       g_route_cnt = 0;
#define HTTP_INBUF_SIZE  256   // 自己按需调大
// map CGI index -> path (for GET dispatch)
static const char* g_cgi_paths[HTTP_ROUTE_MAX];

// Per-request context (no malloc)
typedef struct {
  bool      used;
  uint32_t  token;                             // id in /__dyn/<token>.json
  char      dyn_uri[32];                       // returned to httpd
  char      hdr[HTTP_HEADER_MAX];              // composed header (when included)
  char      body[HTTP_OUTBUF_SIZE];            // body out buffer
  int       body_len;
  int       status;                            // HTTP status to report
  char      ctype[48];                         // content-type
  // POST accumulation
  char      post_uri[128];                     // original POST URI
  char      ctype_in[48];                      // captured request Content-Type
  int       post_len;
  int       err_status;                        // 0 ok, else 4xx code
  // parsed form params (for x-www-form-urlencoded)
  http_kv_t form[16];
  int       form_cnt;
  void*    conn;
  char      in[HTTP_INBUF_SIZE+1];  // ★ 新增：POST 输入缓冲（+1 留 '\0'）
  int       in_len;                 // ★ 新增：POST 输入长度
} http_ctx_t;

static http_ctx_t g_ctx[HTTP_CTX_MAX];
static uint32_t   g_next_token = 1;

// ====== utils ======
static uint32_t new_token(void){ if(++g_next_token==0) g_next_token=1; return g_next_token; }
static http_ctx_t* ctx_alloc(uint32_t t){ for(int i=0;i<HTTP_CTX_MAX;i++) if(!g_ctx[i].used){ memset(&g_ctx[i],0,sizeof(g_ctx[i])); g_ctx[i].used=true; g_ctx[i].token=t; return &g_ctx[i]; } return NULL; }
static http_ctx_t* ctx_find(uint32_t t){ for(int i=0;i<HTTP_CTX_MAX;i++) if(g_ctx[i].used && g_ctx[i].token==t) return &g_ctx[i]; return NULL; }
static void ctx_free(http_ctx_t* c){ if(c) memset(c,0,sizeof(*c)); }

static int hexv(char c){ if(c>='0'&&c<='9')return c-'0'; if(c>='a'&&c<='f')return 10+c-'a'; if(c>='A'&&c<='F')return 10+c-'A'; return -1; }
static void url_decode_inplace(char* s){ char* w=s; for(char* r=s; *r; ){ if(*r=='%'){ int a=hexv(r[1]), b=hexv(r[2]); if(a>=0&&b>=0){ *w++=(char)((a<<4)|b); r+=3; } else { *w++=*r++; } } else if(*r=='+'){ *w++=' '; r++; } else { *w++=*r++; } } *w=0; }

// response helpers
static void res_init(http_ctx_t* c, http_res_t* r){ r->status=200; r->content_type="application/json"; r->out=c->body; r->cap=sizeof(c->body); r->len=0; }
static void res_write(http_res_t* r, const char* s){ int n=(int)strlen(s); if(r->len+n>r->cap) n=r->cap-r->len; if(n>0){ memcpy(r->out+r->len,s,n); r->len+=n; } }
static void res_writef(http_res_t* r, const char* fmt, ...){ va_list ap; va_start(ap,fmt); int n=vsnprintf(r->out+r->len, r->cap-r->len, fmt, ap); va_end(ap); if(n>0){ if(r->len+n>r->cap) n=r->cap-r->len; r->len+=n; } }

const char* http_kv_get(const http_req_t* rq, const char* k){ for(int i=0;i<rq->qcount;i++) if(strcmp(rq->query[i].key,k)==0) return rq->query[i].val; return NULL; }
int http_kv_get_int(const http_req_t* rq, const char* k, int d){ const char* v=http_kv_get(rq,k); return v?atoi(v):d; }
void http_res_set_status(http_res_t* r, int s){ r->status=s; }
void http_res_set_type(http_res_t* r, const char* t){ r->content_type=t; }

// map status -> reason
static const char* reason(int s){
  switch(s){ case 200:return "OK"; case 201:return "Created"; case 204:return "No Content";
    case 400:return "Bad Request"; case 403:return "Forbidden"; case 404:return "Not Found";
    case 413:return "Payload Too Large"; case 415:return "Unsupported Media Type"; case 500:return "Internal Server Error"; default:return "OK"; }
}

// compose full header into ctx->hdr and prepend semantics
static void build_header(http_ctx_t* c){
  int n = snprintf(c->hdr, sizeof(c->hdr),
    "HTTP/1.1 %d %s\r\nServer: lwIP-httpd/mini\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
    c->status ? c->status : 200, reason(c->status?c->status:200), c->ctype[0]?c->ctype:"application/octet-stream", c->body_len);
  (void)n;
}

// ====== Routing registry ======
void HTTP_API_register(http_method_t m, const char* path, route_fn fn){ if(g_route_cnt<HTTP_ROUTE_MAX){ g_routes[g_route_cnt++] = (route_t){m,path,fn}; } }


// ====== GET via CGI ======
static char dyn_path_buf[32];
static const char* cgi_dispatch(int idx, int n, char* param[], char* value[]){
    printf("[httpapi--------2] CGI hit idx=%d path=%s params=%d\n", idx, g_cgi_paths[idx], n);
  const char* path = g_cgi_paths[idx];
  uint32_t tok = new_token(); http_ctx_t* c = ctx_alloc(tok);
  if(!c){ snprintf(c->dyn_uri, sizeof(c->dyn_uri), "/__dyn/%lu.json", (unsigned long)tok);
    return c->dyn_uri;

}

  // fill request
  http_req_t rq = {0}; rq.method = HM_GET; rq.uri = path; rq.qcount = 0;
  for(int i=0;i<n && i<16;i++){ url_decode_inplace(param[i]); url_decode_inplace(value[i]); rq.query[rq.qcount++] = (http_kv_t){ param[i], value[i] }; }

  // find handler
  route_fn fn=NULL; for(int i=0;i<g_route_cnt;i++) if(g_routes[i].m==HM_GET && strcmp(g_routes[i].path,path)==0){ fn=g_routes[i].fn; break; }

  // run
  http_res_t rs; res_init(c,&rs);
  if(fn) fn(&rq,&rs); else { rs.status=404; rs.content_type="text/plain"; res_write(&rs,"404 Not Found\n"); }

  // stash to ctx
  c->body_len = rs.len; memcpy(c->body, rs.out, rs.len);
  c->status   = rs.status; strncpy(c->ctype, rs.content_type?rs.content_type:"application/octet-stream", sizeof(c->ctype)-1);
  snprintf(c->dyn_uri, sizeof(c->dyn_uri), "/__dyn/%lu.json", (unsigned long)tok);
  snprintf(dyn_path_buf, sizeof(dyn_path_buf), "%s", c->dyn_uri);
  return dyn_path_buf;
}

// ====== POST hooks ======
// Parse header fields we care about (very small lexer)
static void parse_headers(const char* hdr, int hlen, http_ctx_t* c){
  if(!hdr||hlen<=0||!c) return;
  const char* end = hdr + hlen;
  const char* p = hdr;
  while(p<end){
    // find line end
    const char* nl = memchr(p,'\n', (size_t)(end-p)); if(!nl) nl=end; const char* line = p; int len=(int)(nl-p);
    if(len>0){
      if((len>13) && strncasecmp(line, "Content-Type:", 13)==0){
        const char* v = line+13; while(*v==' '||*v=='\t') v++; int l = (int)((nl>v?nl:v)-v); if(l> (int)sizeof(c->ctype_in)-1) l=(int)sizeof(c->ctype_in)-1; memcpy(c->ctype_in,v,l); c->ctype_in[l]=0; // strip CR
        char* cr=strchr(c->ctype_in,'\r'); if(cr) *cr=0;
      }
    }
    p = nl+1;
  }
}

static void parse_form_urlencoded(http_ctx_t* c){
  // Convert body into key/value pairs in-place (use c->form)
  c->form_cnt = 0;
  char* s = c->body; s[c->post_len] = 0;
  char* p = s;
  while(p && *p && c->form_cnt < 16){
    char* amp = strchr(p,'&'); if(amp) *amp=0; // key=val\0
    char* eq  = strchr(p,'=');
    if(eq){ *eq=0; url_decode_inplace(p); url_decode_inplace(eq+1);
      c->form[c->form_cnt++] = (http_kv_t){ p, eq+1 };
    }
    if(!amp) break; p = amp+1;
  }
}
err_t httpd_post_begin(void *connection, const char *uri, const char *http_hdr,
                       u16_t hdr_len, int content_len, char *response_uri,
                       u16_t response_uri_len, u8_t *post_auto_wnd){
  printf("[POST] begin uri=%s content_len=%d\n", uri?uri:"", content_len);                      
  (void)connection; (void)content_len; (void)post_auto_wnd;
  uint32_t tok = new_token(); http_ctx_t* c = ctx_alloc(tok);
  if(!c){ snprintf(response_uri,response_uri_len, "/__dyn/0.json"); return ERR_OK; }
  strncpy(c->post_uri, uri?uri:"/", sizeof(c->post_uri)-1);
  parse_headers(http_hdr, hdr_len, c);
//   c->post_len=0; 
  c->in_len = 0;
  c->err_status=0;
  c->form_cnt=0;
  snprintf(c->dyn_uri, sizeof(c->dyn_uri), "/__dyn/%lu.json", (unsigned long)tok);
  snprintf(response_uri, response_uri_len, "%s", c->dyn_uri);
  c->conn = connection;
  return ERR_OK;
}
// --- 放在 lwip_http_api.c 顶部的静态声明区（或与其它 static 工具函数放一起）---
static http_ctx_t* ctx_by_conn(void* conn) {
  for (int i = 0; i < HTTP_CTX_MAX; ++i) {
    if (g_ctx[i].used && g_ctx[i].conn == conn) return &g_ctx[i];
  }
  return NULL;
}
err_t httpd_post_receive_data(void *connection, struct pbuf *p) {
  http_ctx_t* c = ctx_by_conn(connection);
  if (!c) { if (p) pbuf_free(p); return ERR_OK; }
  if (!p) return ERR_OK;

  const int cap = (int)sizeof(c->in) - 1;      // 留 '\0'
  for (struct pbuf* q = p; q; q = q->next) {
    if (c->in_len >= cap) { c->err_status = 413; continue; }
    int cp = q->len;
    if (c->in_len + cp > cap) cp = cap - c->in_len;
    if (cp > 0) { memcpy(c->in + c->in_len, q->payload, (size_t)cp); c->in_len += cp; }
  }
  c->in[c->in_len] = 0;
  pbuf_free(p);
#if LWIP_HTTPD_POST_MANUAL_WND
  httpd_post_data_recved(connection, p ? p->tot_len : 0);
#endif
  return ERR_OK;
}

static void parse_form_urlencoded_into(http_ctx_t* c, char* buf){
  c->form_cnt = 0;
  char* p = buf;
  while (p && *p && c->form_cnt < 16) {
    char* amp = strchr(p, '&'); if (amp) *amp = 0;
    char* eq  = strchr(p, '=');
    if (eq) {
      *eq = 0;
      url_decode_inplace(p);
      url_decode_inplace(eq+1);
      c->form[c->form_cnt++] = (http_kv_t){ p, eq+1 };
    }
    if (!amp) break;
    p = amp + 1;
  }
}

// --- 完整的 httpd_post_finished 版本 ---
void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){
  http_ctx_t* c = ctx_by_conn(connection);
  if (!c) return;

  http_req_t rq = {0};
  rq.method       = HM_POST;
  rq.uri          = c->post_uri[0] ? c->post_uri : "/";
  rq.body         = c->in;               // ★ 用输入缓冲
  rq.body_len     = c->in_len;
  rq.content_type = c->ctype_in;
  rq.qcount       = 0;

  if (c->ctype_in[0] &&
      strncasecmp(c->ctype_in, "application/x-www-form-urlencoded", 33) == 0) {
    c->in[c->in_len] = 0;                // 在 input 缓冲上解析表单
    parse_form_urlencoded_into(c, c->in); // ★ 新增一个按缓冲解析的函数（见下）
    rq.qcount = c->form_cnt;
    for (int i = 0; i < c->form_cnt; ++i) rq.query[i] = c->form[i];
  }

  route_fn fn = NULL;
  for (int i = 0; i < g_route_cnt; ++i)
    if (g_routes[i].m==HM_POST && strcmp(g_routes[i].path, c->post_uri)==0) { fn=g_routes[i].fn; break; }

  http_res_t rs; res_init(c, &rs);       // ★ out 指向 c->body（输出缓冲）
  if (c->err_status) {
    rs.status = 413; rs.content_type = "application/json";
    res_writef(&rs, "{\"error\":%d,\"msg\":\"payload too large\"}\n", c->err_status);
  } else if (!fn) {
    rs.status = 404; rs.content_type = "application/json";
    res_write(&rs, "{\"error\":404,\"msg\":\"Not Found\"}\n");
  } else {
    fn(&rq, &rs);                         // 业务 handler 写入 rs.out（=c->body）
  }

  c->body_len = rs.len;
  c->status   = rs.status ? rs.status : 200;
  strncpy(c->ctype, rs.content_type ? rs.content_type : "application/octet-stream",
          sizeof(c->ctype)-1);

  if (response_uri && response_uri_len)
    snprintf(response_uri, response_uri_len, "%s", c->dyn_uri);  // 再写一次，确保生效
}

// ====== FS custom: serve /__dyn/<token>.json with full header ======
int fs_open_custom(struct fs_file *file, const char *name){
  if (strncmp(name, "/__dyn/", 7) == 0) {
    const char* p = name + 7; char* e = NULL;
    unsigned long id = strtoul(p, &e, 10);
    http_ctx_t* c = ctx_find((uint32_t)id);
    if (!c) return 0;

    // 1) 先生成 HTTP/1.1 头（Content-Length 用 “body_len”）
    build_header(c);
    int hlen = (int)strlen(c->hdr);

    // 2) 确保 header + body 能塞进同一块 c->body（可按需调大 HTTP_OUTBUF_SIZE）
    if (hlen + c->body_len > (int)sizeof(c->body)) {
      // 裁剪 body，避免越界
      c->body_len = (int)sizeof(c->body) - hlen;
      if (c->body_len < 0) c->body_len = 0;
    }

    // 3) 把 body 后移，把 header 放到最前面
    memmove(c->body + hlen, c->body, (size_t)c->body_len);
    memcpy(c->body, c->hdr, (size_t)hlen);

    // 4) 告诉 httpd：file->data 就是完整的 “header+body”
    memset(file, 0, sizeof(*file));
    file->data  = c->body;                          // ← 直接给数据指针
    file->len   = (u16_t)(hlen + c->body_len);
    file->index = 0;

#if LWIP_HTTPD_FILE_FLAGS
    file->flags |= FS_FILE_FLAGS_HEADER_INCLUDED;    // 我们已经包含完整 HTTP 头
#endif

#if LWIP_HTTPD_FILE_STATE
    file->state = c;                                 // 便于 close 时释放上下文
#endif
    return 1;                                        // 由我们处理
  }
  return 0;
}

void fs_close_custom(struct fs_file *file) {
#if LWIP_HTTPD_FILE_STATE
  http_ctx_t* c = (http_ctx_t*)file->state;
  if (c) ctx_free(c);         // 把 c->used 复位，释放槽位
  file->state = NULL;
#else
  LWIP_UNUSED_ARG(file);
#endif
}

#if LWIP_HTTPD_DYNAMIC_FILE_READ
int fs_read_custom(struct fs_file *file, char *buffer, int count){
//   http_ctx_t* c = (http_ctx_t*)file->pextension; if(!c) return FS_READ_EOF;
  http_ctx_t* c = (http_ctx_t*)file->state;
  const int total = (int)strlen(c->hdr) + c->body_len;
  int idx = (int)file->index;
  if(idx >= total) return FS_READ_EOF;
  int n = count; if(idx + n > total) n = total - idx;
  // first part: header
  int hlen = (int)strlen(c->hdr);
  if(idx < hlen){
    int hcopy = n; if(idx + hcopy > hlen) hcopy = hlen - idx;
    memcpy(buffer, c->hdr + idx, hcopy);
    // remaining from body?
    int rest = n - hcopy;
    if(rest>0){ memcpy(buffer + hcopy, c->body, rest); }
  } else {
    // only body
    int boff = idx - hlen;
    memcpy(buffer, c->body + boff, n);
  }
  file->index += (u32_t)n;
  if(file->index >= (u32_t)total) return FS_READ_EOF;
  return n;
}
#endif

// ====== Init: build CGI table from GET routes and enable system ======
void HTTP_API_init(void){
  // allow user to register
  HTTP_API_user_routes();
  // build CGI table
  static tCGI cgi[HTTP_ROUTE_MAX]; int n=0;
  for(int i=0;i<g_route_cnt;i++) if(g_routes[i].m==HM_GET){ cgi[n].pcCGIName = g_routes[i].path; cgi[n].pfnCGIHandler = cgi_dispatch; g_cgi_paths[n] = g_routes[i].path; n++; }
  if(n>0) http_set_cgi_handlers(cgi, n);

  printf("[httpapi--------1] routes=%d, get_cgi=%d\n", g_route_cnt, n);
}


#include <ctype.h>   // tolower
#include <stdarg.h>
#include <string.h>

// ——小工具：大小写不敏感比较——
static int istr_eq(const char* a, const char* b){
  if(!a||!b) return 0;
  while(*a && *b){
    if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return 0;
    ++a; ++b;
  }
  return *a==0 && *b==0;
}

// —— JSON 直取（仅支持最常用场景：key->string / key->int/bool），零依赖第三方库 ——
// 跳过空白
static int json_skip_ws(const char* p, const char* end, const char** np){
  while (p<end && (*p==' '||*p=='\t'||*p=='\r'||*p=='\n')) ++p;
  *np = p; return p<end;
}

static int json_find_key(const char* buf, int len, const char* key,
                         const char** vstart, const char** vend, int* v_is_string){
  if(!buf||!key||!*key) return 0;
  const char* p = buf;
  const char* end = buf + len;
  const int klen = (int)strlen(key);

  while (p < end) {
    const char* q = (const char*)memchr(p, '"', (size_t)(end - p));
    if (!q || q + 1 + klen + 1 >= end) break;

    if (q[1] == key[0] && !memcmp(q+1, key, (size_t)klen) && q[1+klen] == '"') {
      const char* colon = (const char*)memchr(q + 1 + klen + 1, ':', (size_t)(end - (q + 1 + klen + 1)));
      if (!colon) return 0;

      const char* v; if (!json_skip_ws(colon + 1, end, &v)) return 0;

      if (*v == '"') { // string
        v++; const char* s=v; int esc=0;
        while (v<end) {
          char c=*v++;
          if (esc) { esc=0; continue; }
          if (c=='\\') { esc=1; continue; }
          if (c=='"') { *vstart=s; *vend=v-1; *v_is_string=1; return 1; }
        }
        return 0;
      } else {        // number / bool / null
        const char* s=v;
        while (v<end && !isspace((unsigned char)*v) && *v!=',' && *v!='}' && *v!=']') ++v;
        *vstart=s; *vend=v; *v_is_string=0; return 1;
      }
    }
    p = q + 1;
  }
  return 0;
}

static int json_get_string(const char* buf, int len, const char* key, char* out, int cap){
  if(!out||cap<=0) return 0; out[0]=0;
  const char *vs, *ve; int is_str=0;
  if(!json_find_key(buf, len, key, &vs, &ve, &is_str) || !is_str) return 0;
  int w=0, esc=0; const char* p=vs;
  while (p<ve && w<cap-1) {
    char c=*p++;
    if (esc){ out[w++]=c; esc=0; }
    else if (c=='\\') esc=1;
    else out[w++]=c;
  }
  out[w]=0; return w;
}

static int json_get_int(const char* buf, int len, const char* key, int* outv){
  const char *vs, *ve; int is_str=0;
  if(!outv) return 0;
  if(!json_find_key(buf, len, key, &vs, &ve, &is_str)) return 0;
  if (is_str) return 0;
  if ((ve - vs) >= 4 && !memcmp(vs, "true", 4))  { *outv=1; return 1; }
  if ((ve - vs) >= 5 && !memcmp(vs, "false", 5)) { *outv=0; return 1; }
  int neg=0; const char* p=vs; long v=0; int any=0;
  if (p<ve && *p=='-'){ neg=1; ++p; }
  while (p<ve && isdigit((unsigned char)*p)) { v = v*10 + (*p - '0'); ++p; any=1; }
  if (!any) return 0;
  *outv = neg ? -(int)v : (int)v; return 1;
}

static int content_is_json(const http_req_t* r){
  const char* c = (r && r->content_type) ? r->content_type : "";
  return (c && strstr(c, "application/json") != NULL);
}

// —— 统一取参 ——
// 优先 JSON，其次 query/form
int httpx_get_int(const http_req_t* r, const char* key, int defv){
  int v = defv;
  if (r && key && content_is_json(r) && r->body && r->body_len>0)
    if (json_get_int(r->body, r->body_len, key, &v)) return v;
  return http_kv_get_int(r, key, defv);
}

int httpx_get_bool(const http_req_t* r, const char* key, int defv){
  int v = defv;
  if (r && key && content_is_json(r) && r->body && r->body_len>0)
    if (json_get_int(r->body, r->body_len, key, &v)) return v ? 1 : 0;
  const char* s = http_kv_get(r, key);
  if (!s) return defv;
  if (istr_eq(s,"1") || istr_eq(s,"true")  || istr_eq(s,"yes")) return 1;
  if (istr_eq(s,"0") || istr_eq(s,"false") || istr_eq(s,"no" )) return 0;
  return defv;
}

int httpx_get_str(const http_req_t* r, const char* key, char* out, int cap){
  if (!out || cap<=0) return 0; out[0]=0;
  if (r && key && content_is_json(r) && r->body && r->body_len>0) {
    int n = json_get_string(r->body, r->body_len, key, out, cap);
    if (n>0) return n;
  }
  const char* s = http_kv_get(r, key);
  if (!s) return 0;
  int w=0; while (s[w] && w<cap-1) { out[w]=s[w]; ++w; } out[w]=0; return w;
}

// —— 路径参数：/a/:id/b ——
// pattern 形如 "/api/print/:id"
static int match_and_capture(const char* path, const char* pattern,
                             const char* name, char* out, int cap){
  if (!path||!pattern||!name||!out||cap<=0) return 0;
  out[0]=0;
  const char *p = path, *q = pattern;
  char cur[32]={0}; int have_slot=0;
  const char* slot_start=NULL;

  while (*p || *q) {
    if (*q == ':') { // 读占位符名
      q++;
      int w=0; while (*q && *q!='/') { if (w<(int)sizeof(cur)-1) cur[w++]=*q; q++; }
      cur[w]=0; have_slot=1; slot_start=p;
      while (*p && *p!='/') p++; // path 前进到下一段
      if (!strcmp(cur, name)) {
        int n = (int)(p - slot_start); if (n >= cap) n = cap-1;
        memcpy(out, slot_start, (size_t)n); out[n]=0;
      }
      if (*q=='/' && *p=='/') { q++; p++; }
      continue;
    }
    if (*q != *p) return 0;
    if (*q==0) break;
    q++; p++;
  }
  return have_slot ? 1 : 0;
}

int httpx_path_param(const http_req_t* r, const char* pattern,
                     const char* name, char* out, int cap){
  if (!r) return 0;
  const char* path = r->uri;  // 你的字段名为 uri
  return match_and_capture(path ? path : "", pattern, name, out, cap);
}

// —— 统一 JSON 回复 —— 
static void json_escape_min(const char* in, char* out, int cap){
  int w=0; const char* p = in ? in : "";
  while (*p && w+2<cap) {
    if (*p=='"' || *p=='\\') { out[w++]='\\'; out[w++]=*p++; }
    else out[w++]=*p++;
  }
  out[w]=0;
}

void httpx_res_json(http_res_t* s){
  http_res_set_type(s, "application/json");
}

static int vappend(http_res_t* s, const char* fmt, va_list ap){
  if (!s || !s->out || s->cap<=0) return -1;
  int n = vsnprintf(s->out + s->len, s->cap - s->len, fmt, ap);
  if (n < 0) return n;
  if (n > s->cap - s->len) n = s->cap - s->len; // 截断保护
  s->len += n; return n;
}

int httpx_ok(http_res_t* s){
  httpx_res_json(s);
  int n = snprintf(s->out + s->len, s->cap - s->len, "{\"ok\":true}\n");
  if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  return 0;
}

int httpx_okf(http_res_t* s, const char* fmt, ...){
  httpx_res_json(s);
  int n = snprintf(s->out + s->len, s->cap - s->len, "{\"ok\":true");
  if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  if (fmt && *fmt){
    n = snprintf(s->out + s->len, s->cap - s->len, ",");
    if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
    va_list ap; va_start(ap, fmt);
    vappend(s, fmt, ap);
    va_end(ap);
  }
  n = snprintf(s->out + s->len, s->cap - s->len, "}\n");
  if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  return 0;
}

int httpx_err(http_res_t* s, int code, const char* msg){
  httpx_res_json(s);
  char esc[256]; json_escape_min(msg, esc, sizeof esc);
  int n = snprintf(s->out + s->len, s->cap - s->len,
                   "{\"ok\":false,\"code\":%d,\"msg\":\"%s\"}\n", code, esc);
  if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  return 0;
}

int httpx_errf(http_res_t* s, int code, const char* fmt, ...){
  httpx_res_json(s);
  int n = snprintf(s->out + s->len, s->cap - s->len, "{\"ok\":false,\"code\":%d", code);
  if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  if (fmt && *fmt){
    n = snprintf(s->out + s->len, s->cap - s->len, ",\"msg\":\"");
    if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
    va_list ap; va_start(ap, fmt);
    char tmp[256]; vsnprintf(tmp, sizeof tmp, fmt, ap); va_end(ap);
    char esc[512]; json_escape_min(tmp, esc, sizeof esc);
    n = snprintf(s->out + s->len, s->cap - s->len, "%s", esc);
    if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
    n = snprintf(s->out + s->len, s->cap - s->len, "\"");
    if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  }
  n = snprintf(s->out + s->len, s->cap - s->len, "}\n");
  if (n>0) s->len += (n < s->cap - s->len ? n : s->cap - s->len);
  return 0;
}

// —— 全局钩子（可选） ——
static httpx_before_cb g_before = 0;
static httpx_after_cb  g_after  = 0;

void httpx_set_before(httpx_before_cb cb){ g_before = cb; }
void httpx_set_after (httpx_after_cb  cb){ g_after  = cb; }

int  httpx_run_before(const http_req_t* r, http_res_t* s){ return g_before ? g_before(r, s) : 0; }
void httpx_run_after (const http_req_t* r, http_res_t* s, int ret){ if (g_after) g_after(r, s, ret); }

// —— CORS 预留点（若底层支持添加响应头，可在此处调用） ——
void httpx_apply_cors(http_res_t* s, const char* allow_origin){
  (void)s; (void)allow_origin;
  // 例如：
  // http_res_add_header(s, "Access-Control-Allow-Origin", allow_origin ? allow_origin : "*");
  // http_res_add_header(s, "Access-Control-Allow-Headers", "Content-Type, Authorization");
  // http_res_add_header(s, "Access-Control-Allow-Methods", "GET,POST,OPTIONS");
}